using System;
using System.ComponentModel;
using System.Reflection;
using System.Web;
using Edge.Common.ComponentModel.Parameters;
using Edge.Common.ComponentModel.Exceptions;
using Edge.Common.ComponentModel.Exceptions;

namespace Edge.Common.ComponentModel.Parameters
{
    /// <summary>
    /// 
    /// TODO: add validation support
    /// 
    /// Marks a field or property as being bound to a specific parameter present in the
    /// <see cref="System.Web.HttpRequest"/>. This attribute is normally only
    /// applied to subclasses of <see cref="System.Web.UI.Page"/>
    /// </summary>
    /// <example>
    /// Here a simple page class marks field with the attribute, and then
    /// calls the static WebParameterAttribute.SetValues() method to
    /// automatically load the fields with value from Request.Form or Request.QueryString
    /// (depending on what was used to submit the form). Note that since
    /// parameter binding in this example is done both on first-request
    /// and on postback, this page must always be either linked to supplying
    /// data in the querystring, or cross-posted to with the data in the Form.
    /// <code><![CDATA[
    /// public class BoundParameterDemo : System.Web.UI.Page{
    ///		[WebParameter()]
    ///		protected string FirstName;
    ///
    ///		[WebParameter("Last_Name")]
    ///		protected string LastName;
    ///
    ///		[WebParameter(IsRequired=true)]
    ///		protected int CustomerID;
    ///
    ///		private void Page_Load(object sender, System.EventArgs e) {
    ///			WebParameterAttribute.SetValues(this, Request);
    ///		}
    ///	}
    /// ]]>
    /// </code>
    /// </example>
    [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
    public class WebParameterAttribute : AbstractParametr
    {
        #region Declarations

        #endregion

        #region Constructors
        /// <summary>
        /// Creates a new WebParameterAttribute to load a field from an identically-named
        /// parameter in the Form/QueryString collection, if it exists.
        /// The parameter has no default value, and is not required
        /// </summary>
        public WebParameterAttribute()
            : base()
        {
        }

        /// <summary>
        /// Creates a new WebParameterAttribute to load a field from the given parameter name
        /// The parameter has no default value, and is not required
        /// </summary>
        /// <param name="paramName">The key of a parameter in the Form or QueryString collections</param>
        public WebParameterAttribute(string paramName)
            : base(paramName)
        {
        }
        #endregion

        public static string GetQueryString(object target)
        {
            System.Type type = target.GetType();
            FieldInfo[] fields = type.GetFields(BindingFlags.Instance | BindingFlags.Public);
            PropertyInfo[] properties = type.GetProperties(BindingFlags.Instance | BindingFlags.Public);
            MemberInfo[] members = new MemberInfo[fields.Length + properties.Length];
            fields.CopyTo(members, 0);
            properties.CopyTo(members, fields.Length);
            string query = "";
            for (int f = 0; f < members.Length; f++)
            {
                string q = GetQueryValue(members[f], target);
                if (q != "")
                    query += "&" + q;
            }
            return query;
        }

        private static string GetQueryValue(MemberInfo member, object target)
        {
            WebParameterAttribute[] attribs;
            WebParameterAttribute attrib;
            string paramValue = "";
            string paramName = "";
            try
            {
                attribs = (QueryParameterAttribute[])member.GetCustomAttributes(typeof(QueryParameterAttribute), true);
                if (attribs != null && attribs.Length > 0)
                {
                    // Just make sure we're not going after an indexed property
                    if (member.MemberType == MemberTypes.Property)
                    {
                        attrib = attribs[0];
                        paramName = attrib.ParameterName ?? member.Name;
                        paramValue = ((PropertyInfo)member).GetValue(target, new Object[0]).ToString();
                    }
                }
            }
            catch
            {
                paramName = "";
                paramValue = "";
            }
            if (paramName != "" && paramValue != "")
                return paramName + "=" + paramValue;
            else
                return "";
        }
        /// <summary>
        /// Retrieves an item either from the Query or POST collections, depending on the
        /// mode of the request, or performs custom retrieval in derived classes
        /// </summary>
        /// <remarks>Typically a subclass would overide this to bind a parameter
        /// specifically to - say - Request.Form, or even an item in the Cookies
        /// collection.
        /// The parameter name has to be passed since some attributes have
        /// no ParameterName, marking them as using the same name as
        /// the member (which is unknown to the attribute)</remarks>
        protected virtual string GetValue(string paramName, System.Web.HttpRequest request)
        {
            if (request.HttpMethod.ToLower() == "post")
                return request.Form[paramName];
            else
                return request.QueryString[paramName];
        }

        /// <summary>
        /// Sets public properties and fields on <c>target</c> that are marked with
        /// <see cref="WebParameterAttribute"/> to the corresponding values retrieved from
        /// <c>request</c>, or a default value as set on the attribute
        /// </summary>
        /// <param name="target">The object (typically a <see cref="System.Web.UI.Page"/>) being bound</param>
        /// <param name="request">The <see cref="System.Web.HttpRequest"/> to load the data from.
        /// The attribute determines whether data is loaded from request.Form, request.QueryString
        /// or other parts of request</param>
        public static void SetValues(object target, System.Web.HttpRequest request)
        {
            System.Type type = target.GetType();
            FieldInfo[] fields = type.GetFields(BindingFlags.Instance | BindingFlags.Public);
            PropertyInfo[] properties = type.GetProperties(BindingFlags.Instance | BindingFlags.Public);
            MemberInfo[] members = new MemberInfo[fields.Length + properties.Length];
            fields.CopyTo(members, 0);
            properties.CopyTo(members, fields.Length);

            for (int f = 0; f < members.Length; f++)
                SetValue(members[f], target, request);
        }


        /// <summary>
        /// call SetValues for current request
        /// </summary>
        /// <param name="target"></param>
        public static void SetValues(object target)
        {
            SetValues(target, HttpContext.Current.Request);
        }

        /// <summary>
        /// Examines a single <c>member</c> (a property or field) for <see cref="WebParameterAttribute"/>.
        /// If so marked then the member is set on <c>target</c> with the relevant value
        /// retrieved from <c>request</c>, or the default value provided in the attribute
        /// </summary>
        /// <param name="target">The object (typically a <see cref="System.Web.UI.Page"/>) being bound</param>
        /// <param name="request">The <see cref="System.Web.HttpRequest"/> to load the data from.
        /// The attribute determines whether data is loaded from request.Form, request.QueryString
        /// or other parts of request</param>
        public static void SetValue(MemberInfo member, object target, System.Web.HttpRequest request)
        {
            WebParameterAttribute[] attribs;
            WebParameterAttribute attrib;
            TypeConverter converter;
            string paramValue;
            string paramName;
            object typedValue;
            bool usingDefault;

            try
            {
                attribs = (WebParameterAttribute[])member.GetCustomAttributes(typeof(WebParameterAttribute), true);
                if (attribs != null && attribs.Length > 0)
                {
                    // Just make sure we're not going after an indexed property
                    if (member.MemberType == MemberTypes.Property)
                    {
                        ParameterInfo[] ps = ((PropertyInfo)member).GetIndexParameters();
                        if (ps != null && ps.Length > 0)
                            throw new NotSupportedException("Cannot apply WebParameterAttribute to indexed property");
                    }

                    // There should only be one WebParameterAttribute (it's a single-use attribute)
                    attrib = attribs[0];
                    paramName = (attrib.ParameterName != null) ? attrib.ParameterName : member.Name;
                    paramValue = attrib.GetValue(paramName, request);

                    // Handle default value assignment, if required
                    usingDefault = false;
                    if (paramValue == null)
                    {
                        if (attrib.DefaultValue != null)
                        {
                            paramValue = attrib.DefaultValue;
                            usingDefault = true;
                        }
                        else if (!attrib.IsRequired)
                            return;	// Just skip the member
                        else
                            throw new WebParameterException("Missing required parameter", paramName);
                    }

                    // check by MaxLength
                    if (attrib.MaxLength >= 0 && paramValue.Length > attrib.MaxLength)
                        throw new WebParameterException(string.Format("parametr more then {0} symbols", attrib.MaxLength), paramName);


                    // Now assign the loaded value onto the member, using the relevant type converter
                    // Have to perform the assignment slightly differently for fields and properties
                    converter = TypeDescriptor.GetConverter(GetMemberUnderlyingType(member));
                    if (converter == null || !converter.CanConvertFrom(paramValue.GetType()))
                        throw new WebParameterException(String.Format("Could not convert from {0}", paramValue.GetType()), paramName);

                    try
                    {
                        typedValue = converter.ConvertFrom(paramValue);
                        SetMemberValue(member, target, typedValue);
                    }
                    catch
                    {
                        // We catch errors both from the type converter
                        // and from any problems in setting the field/property
                        // (eg property-set rules, security, readonly properties)

                        // If we're not already using the default, but there
                        // is one, and we're allowed to use it for invalid data, give
                        // it a go, otherwise just propagate the error

                        if (!usingDefault && attrib.IsDefaultUsedForInvalid && attrib.DefaultValue != null)
                        {
                            typedValue = converter.ConvertFrom(attrib.DefaultValue);
                            SetMemberValue(member, target, typedValue);
                        }
                        else
                            throw;
                    }
                }
            }
            catch (Exception err)
            {
                throw new WebParameterException("Property/field {0} could not be set from request - " + err.Message, "", err);
            }
        }

        /// <summary>
        /// Sets <c>member</c> on <c>target</c> to <c>value</c>. <c>member</c>
        /// may be a field or a property
        /// </summary>
        private static void SetMemberValue(MemberInfo member, object target, object value)
        {
            switch (member.MemberType)
            {
                case MemberTypes.Field:
                    ((FieldInfo)member).SetValue(target, value);
                    break;

                case MemberTypes.Property:
                    // We've already ensured this isn't an indexed property
                    ((PropertyInfo)member).SetValue(target, value, new Object[0]);
                    break;
            }
        }

        /// <summary>
        /// Retrieves the <see cref="Type"/> that <c>member</c> represents
        /// (that is to say the type of the member on the object, rather
        /// than the subtype of MemberInfo). <c>member</c> may be a
        /// <see cref="FieldInfo"/> or a <see cref="PropertyInfo"/>
        /// </summary>
        private static Type GetMemberUnderlyingType(MemberInfo member)
        {
            switch (member.MemberType)
            {
                case MemberTypes.Field:
                    return ((FieldInfo)member).FieldType;

                case MemberTypes.Property:
                    return ((PropertyInfo)member).PropertyType;

                default:
                    throw new ArgumentException("Expected a FieldInfo or PropertyInfo", "member");
            }
        }
    }
}
